﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.RegularExpressions;

namespace Infrastructure
{
    /// <summary>
    /// File system performance utils, improve on <see cref="Directory.GetFiles(string)"/>.
    /// <remarks>
    /// <para>Enable recursive get file with recursion level limit</para>
    /// <para>Enable to count the number of files in a folder without getting the file to for 20% performance benifit</para>
    /// <para>Enable getting limited number of files (i.e. first 1,000 files in folder) to able to handle folders with more
    /// than 10,000 files in it (<see cref="Directory.GetFiles(string)"/> will take lots of time and memory)</para>
    /// </remarks>
    /// </summary>
    public static class FileSystemUtils
    {
        #region kernel32 functions

        [DllImport("kernel32", CharSet = CharSet.Unicode)]
        private static extern IntPtr FindFirstFile(string lpFileName, out WIN32_FIND_DATA lpFindFileData);

        [DllImport("kernel32", SetLastError = true)]
        private static extern bool FindClose(IntPtr hFindFile);

        [DllImport("kernel32", CharSet = CharSet.Unicode)]
        private static extern bool FindNextFile(IntPtr hFindFile, out WIN32_FIND_DATA lpFindFileData);

        [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        private static extern uint GetShortPathName([MarshalAs(UnmanagedType.LPTStr)] string lpszLongPath,
                                                    [MarshalAs(UnmanagedType.LPTStr)] StringBuilder lpszShortPath,
                                                    uint cchBuffer);

        #endregion

        /// <summary>
        /// Count the number of files in the given folder
        /// </summary>
        /// <param name="folder">the folder to count the files in, '.' for current folder</param>
        /// <returns>the number of files in the given folder</returns>
        /// <exception cref="ArgumentException"><paramref name="folder"/>is null, empty or the folder not exist</exception>
        /// <exception cref="Exception">failed to recurse the file system</exception>
        public static int CountFiles(string folder)
        {
            long size;
            int filesCount;
            int folders;
            RecurseDirectoryForFiles(folder, 0, null, "*", 0, out size, out filesCount, out folders);
            return filesCount;
        }

        /// <summary>
        /// Get the full path of files in the given folder.
        /// <div>return up to <paramref name="limit"/> of files</div>
        /// </summary>
        /// <param name="folder">the folder to count the files in, '.' for current folder</param>
        /// <param name="limit">limit the number of files to return</param>
        /// <returns>the number of files in the given folder</returns>
        /// <exception cref="ArgumentException"><paramref name="folder"/>is null, empty or the folder not exist</exception>
        /// <exception cref="Exception">failed to recurse the file system</exception>
        public static ICollection<string> GetFiles(string folder, int limit)
        {
            return GetFiles(folder, null, limit);
        }

        /// <summary>
        /// Get the full path of files in the given folder.
        /// <div>return up to <paramref name="limit"/> of files</div>
        /// </summary>
        /// <param name="folder">the folder to count the files in, '.' for current folder</param>
        /// <param name="filter"></param>
        /// <param name="limit">limit the number of files to return</param>
        /// <returns>the number of files in the given folder</returns>
        /// <exception cref="ArgumentException"><paramref name="folder"/>is null, empty or the folder not exist</exception>
        /// <exception cref="Exception">failed to recurse the file system</exception>
        public static ICollection<string> GetFiles(string folder, string filter, int limit)
        {
            return GetFiles(folder, 0, filter, limit);
        }


        /// <summary>
        /// Get the full path of files in the given folder.
        /// <div>return up to <paramref name="limit"/> of files</div>
        /// </summary>
        /// <param name="folder">the folder to count the files in, '.' for current folder</param>
        /// <param name="depth">the number of levels to recurse, only the given folder - 0</param>
        /// <param name="filter"></param>
        /// <param name="limit">limit the number of files to return</param>
        /// <returns>the number of files in the given folder</returns>
        /// <exception cref="ArgumentException"><paramref name="folder"/>is null, empty or the folder not exist</exception>
        /// <exception cref="Exception">failed to recurse the file system</exception>
        public static ICollection<string> GetFiles(string folder, int depth, string filter, int limit)
        {
            filter = !string.IsNullOrEmpty(filter) ? filter : "*";

            long size;
            int filesCount;
            int folders;
            ICollection<string> files = new List<string>();
            RecurseDirectoryForFiles(folder, depth, files, filter, limit, out size, out filesCount, out folders);
            return files;
        }

        /// <summary>
        /// Delete the given folder if empty
        /// </summary>
        /// <param name="path"></param>
        public static void DeleteEmptyFolder(string path)
        {
            if (!Directory.Exists(path)) return;
            if (Directory.GetFiles(path).Length == 0 && Directory.GetDirectories(path).Length == 0)
            {
                Directory.Delete(path);
            }
        }

        public static void MoveFile(string fileName, string destFolder, bool overwrite)
        {
            if (!File.Exists(fileName)) return;

            if (!Directory.Exists(Path.GetDirectoryName(destFolder)))
            {
                Directory.CreateDirectory(destFolder);
                File.Move(fileName, destFolder);
            }
            else
            {
                if (File.Exists(destFolder) && overwrite)
                {
                    File.Delete(destFolder);
                }

            }
            File.Move(fileName, destFolder);
        }

        /// <summary>
        /// The ToLongPathNameToShortPathName function retrieves the short path form of a specified long input path
        /// </summary>
        /// <param name="longName">The long name path</param>
        /// <returns>A short name path string</returns>
        public static string ToShortPathName(string longName)
        {
            const uint bufferSize = 256;

            // don´t allocate stringbuilder here but outside of the function for fast access
            var shortNameBuffer = new StringBuilder((int)bufferSize);

            uint result = GetShortPathName(longName, shortNameBuffer, bufferSize);

            return shortNameBuffer.ToString();
        }

        /// <summary>
        /// Get the full path of folders in the given directory.
        /// <div>return up to <paramref name="limit"/> of files</div>
        /// </summary>
        /// <param name="folder">the folder to count the files in, '.' for current folder</param>
        /// <param name="depth">the number of levels to recurse, only the given folder - 0</param>
        /// <param name="limit">limit the number of files to return</param>
        /// <returns>the number of files in the given folder</returns>
        /// <exception cref="ArgumentException"><paramref name="folder"/>is null, empty or the folder not exist</exception>
        /// <exception cref="Exception">failed to recurse the file system</exception>
        public static ICollection<string> GetDirectories(string folder, int depth, int limit)
        {
            int folders;
            ICollection<string> files = new List<string>();
            RecurseDirectoryForFolders(folder, depth, files, "*", limit, out folders);
            return files;
        }

        private static IEnumerable<Regex> BuildFiler(string filter)
        {
            // make sure spec doesn't contain invalid characters
            if (filter.IndexOfAny(new char[] { ':', '<', '>', '/', '\\' }) >= 0)
                throw new ArgumentException("invalid cahracters in wildcard pattern", "filter");

            string[] specs = filter.Split(new char[] { ';', ',' });
            var fileSpecs = new List<Regex>(specs.Length);
            foreach (string spec in specs)
            {
                // trim whitespace off file spec and convert Win32 wildcards to regular expressions
                string pattern = spec.Trim()
                                    .Replace(".", @"\.")
                                    .Replace("*", @".*")
                                    .Replace("?", @".?");
                fileSpecs.Add(new Regex("^" + pattern + "$", RegexOptions.IgnoreCase));
            }
            return fileSpecs;
        }

        /// <summary>
        /// Recurse file system hierarchy to extract directories.
        /// </summary>
        /// <param name="folder">the root folder to recurse</param>
        /// <param name="level">the number of levels to recurse, only the given folder - 0</param>
        /// <param name="files">collection of files path that were found, send null if not interested</param>
        /// <param name="filter">the filter to get files by</param>
        /// <param name="filesLimit">limit the number of files to return (relevant only if <paramref name="files"/> is given)</param>
        /// <param name="size">returns the total size of all the filesCount in the folder in all levels</param>
        /// <param name="filesCount">returns the total number of files in the folder in all levels</param>
        /// <param name="folders">returns the total number of folders in the folder in all levels</param>
        /// <exception cref="ArgumentException"><paramref name="folder"/>is null, empty or the folder not exist</exception>
        /// <exception cref="Exception">failed to recurse the file system</exception>
        private static void RecurseDirectoryForFolders(string folder, int level, ICollection<string> folderList,
                                                       string filter, int filesLimit, out int folderCount)
        {
            if (string.IsNullOrEmpty((folder)))
                throw new ArgumentException("folder is null or empty");
            if (!Directory.Exists(folder))
                throw new ArgumentException("the given folder does not exist: " + folder);

            try
            {
                var INVALID_HANDLE_VALUE = new IntPtr(-1);
                folderCount = 0;
                WIN32_FIND_DATA findData;

                string fullFolderPath = Utils.ConvertToFullPath(folder);
                string searchPath = string.Empty;
                var filters = filter.Split(';');
                foreach (var s in filters)
                {
                    searchPath = string.Format(@"{0}\{1}; ", folder, s);
                }
                searchPath = searchPath.Trim(';', ' ');
                IntPtr findHandle = FindFirstFile(searchPath, out findData);
                if (findHandle != INVALID_HANDLE_VALUE)
                {
                    do
                    {
                        if ((findData.dwFileAttributes & FileAttributes.Directory) != 0)
                        {
                            // folder
                            if (findData.cFileName != "." && findData.cFileName != "..")
                            {
                                folderCount++;
                                string subdirectory = Path.Combine(fullFolderPath, findData.cFileName);
                                //string subdirectory = folder + (folder.EndsWith(@"\") ? string.Empty : @"\") + findData.cFileName;


                                bool shouldAdd = true;

                                // if more levels to search, do recursion
                                if (level != 0)
                                {
                                    int subfolders;
                                    RecurseDirectoryForFolders(subdirectory, level - 1, folderList, filter, filesLimit,
                                                               out subfolders);
                                    folderCount += subfolders;

                                    shouldAdd &= subfolders == 0;
                                }


                                if (shouldAdd && folderList != null)
                                {
                                    folderList.Add(subdirectory);
                                    if (folderList.Count >= filesLimit)
                                        return;
                                }
                            }
                        }
                    } while (FindNextFile(findHandle, out findData));
                    FindClose(findHandle);
                }
            }
            catch (Exception ex)
            {
                throw new Exception("Failed to recurse file system, root: " + folder, ex);
            }
        }

        /// <summary>
        /// Recurse file system hierarchy to extract data.
        /// </summary>
        /// <param name="folder">the root folder to recurse</param>
        /// <param name="level">the number of levels to recurse, only the given folder - 0</param>
        /// <param name="files">collection of files path that were found, send null if not interested</param>
        /// <param name="filter">the filter to get files by</param>
        /// <param name="filesLimit">limit the number of files to return (relevant only if <paramref name="files"/> is given)</param>
        /// <param name="size">returns the total size of all the filesCount in the folder in all levels</param>
        /// <param name="filesCount">returns the total number of files in the folder in all levels</param>
        /// <param name="folders">returns the total number of folders in the folder in all levels</param>
        /// <exception cref="ArgumentException"><paramref name="folder"/>is null, empty or the folder not exist</exception>
        /// <exception cref="Exception">failed to recurse the file system</exception>
        private static void RecurseDirectoryForFiles(string folder, int level, ICollection<string> files, string filter,
                                                     int filesLimit, out long size, out int filesCount, out int folders)
        {
            if (string.IsNullOrEmpty((folder)))
                throw new ArgumentException("folder is null or empty");
            if (!Directory.Exists(folder))
                throw new ArgumentException("the given folder does not exist: " + folder);

            try
            {
                var INVALID_HANDLE_VALUE = new IntPtr(-1);
                size = 0;
                filesCount = 0;
                folders = 0;
                WIN32_FIND_DATA findData;

                string fullFolderPath = Utils.ConvertToFullPath(folder);
                var filters = BuildFiler(filter);
                IntPtr findHandle = FindFirstFile(Path.Combine(folder, "*.*"), out findData);
                if (findHandle != INVALID_HANDLE_VALUE)
                {
                    do
                    {
                        if ((findData.dwFileAttributes & FileAttributes.Directory) != 0)
                        {
                            // folder
                            if (findData.cFileName != "." && findData.cFileName != "..")
                            {
                                folders++;
                                string subdirectory = Path.Combine(fullFolderPath, findData.cFileName);
                                //string subdirectory = folder + (folder.EndsWith(@"\") ? string.Empty : @"\") + findData.cFileName;


                                // if more levels to search, do recursion
                                if (level != 0)
                                {
                                    long subSize;
                                    int subfiles;
                                    int subfolders;
                                    RecurseDirectoryForFiles(subdirectory, level - 1, files, filter, filesLimit,
                                                             out subSize, out subfiles, out subfolders);
                                    size += subSize;
                                    folders += subfolders;
                                    filesCount += subfiles;
                                }
                            }
                        }
                        else
                        {
                            // it's a file, see if any of the filespecs matches it
                            foreach (Regex fileSpec in filters)
                            {
                                string fullFileName = Path.Combine(fullFolderPath, findData.cFileName);
                                // if this spec matches, return this file's info
                                if (!fileSpec.IsMatch(fullFileName))
                                    continue;

                                // File
                                filesCount++;
                                size += findData.nFileSizeLow + findData.nFileSizeHigh * 4294967296;
                                if (files != null)
                                {
                                    files.Add(fullFileName);
                                    if (files.Count >= filesLimit)
                                        return;
                                }
                            }
                        }
                    } while (FindNextFile(findHandle, out findData));
                    FindClose(findHandle);
                }
            }
            catch (Exception ex)
            {
                throw new Exception("Failed to recurse file system, root: " + folder, ex);
            }
        }

    }

    #region FILETIME struct

    [StructLayout(LayoutKind.Sequential)]
    public struct FILETIME
    {
        public uint dwLowDateTime;
        public uint dwHighDateTime;
    } ;

    #endregion

    #region WIN32_FIND_DATA struct

    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
    public struct WIN32_FIND_DATA
    {
        public FileAttributes dwFileAttributes;
        public FILETIME ftCreationTime;
        public FILETIME ftLastAccessTime;
        public FILETIME ftLastWriteTime;
        public int nFileSizeHigh;
        public int nFileSizeLow;
        public int dwReserved0;
        public int dwReserved1;
        [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)]
        public string cFileName;
        [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 14)]
        public string cAlternate;
    }

    #endregion
}